package cn.sherer.beans.weixin;

import java.util.Map;
import org.w3c.dom.Node;
import java.util.HashMap;
import java.io.InputStream;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.w3c.dom.Document;
import javax.xml.XMLConstants;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.qq.weixin.mp.aes.SHA1;
import cn.sherer.utils.RequestUtils;
import java.io.ByteArrayInputStream;
import com.alibaba.fastjson2.JSONObject;
import javax.xml.parsers.DocumentBuilder;
import com.qq.weixin.mp.aes.AesException;
import com.qq.weixin.mp.aes.WXBizMsgCrypt;
import java.io.UnsupportedEncodingException;
import javax.servlet.http.HttpServletRequest;
import java.security.NoSuchAlgorithmException;
import javax.xml.parsers.DocumentBuilderFactory;

import static java.lang.String.valueOf;
import static cn.sherer.utils.DateUtils.*;
import static cn.sherer.utils.StringUtils.*;
import static java.text.MessageFormat.format;
import static com.alibaba.fastjson2.JSONObject.*;

/**
 * 微信公众平台
 */
public class WxSalon {
    private String token = "";
    private String appid = "";
    private String encodingAESKey = "";
    private Boolean isWorkWeixin = false;

    private WxSalonResult resultData;
    private Map<String, Object> params = new HashMap<>();

    public WxSalon(HttpServletRequest request, String token) {
        this(request, token, false);
    }

    public WxSalon(HttpServletRequest request, String token, Boolean isWorkWeixin) {
        this(request, token, "", "", isWorkWeixin);
    }

    public WxSalon(HttpServletRequest request, String token, String appid, String encodingAESKey) {
        this(request, token, appid, encodingAESKey, false);
    }

    public WxSalon(HttpServletRequest request, String token, String appid, String encodingAESKey, Boolean isWorkWeixin) {
        this(new HashMap<>(), token, appid, encodingAESKey, isWorkWeixin);
        RequestUtils requestUtils = new RequestUtils(request);
        this.params.putAll(requestUtils.get());
        if (!requestUtils.getPostData().isEmpty()) {
            try {
                Map<String, String> map = xmlToMap(requestUtils.getPostData());
                if (map != null && !map.isEmpty()) { this.params.putAll(map); }
            } catch (Exception e) {
                Logger.getLogger(this.getClass().getName()).log(Level.SEVERE, "解析 XML 失败", e);
            }
        }
    }

    public WxSalon(Map<String, Object> params, String token) {
        this(params, token, false);
    }

    public WxSalon(Map<String, Object> params, String token, Boolean isWorkWeixin) {
        this(params, token, "", "", isWorkWeixin);
    }

    public WxSalon(Map<String, Object> params, String token, String appid, String encodingAESKey) {
        this(params, token, appid, encodingAESKey, false);
    }

    public WxSalon(Map<String, Object> params, String token, String appid, String encodingAESKey, Boolean isWorkWeixin) {
        this.token = token;
        this.appid = appid;
        this.encodingAESKey = encodingAESKey;
        this.params = params == null ? new HashMap<>() : params;
        this.isWorkWeixin = isWorkWeixin;
    }

    public String run(WxSalonOptions options) {
        // <editor-fold defaultstate="collapsed" desc="获取公共参数">
        String nonce = (valueOf(get("nonce", ""))); // 随机字符串
        String openid = (valueOf(get("openid", ""))); // 用户Openid
        String echostr = (get("echostr", "").toString()); // 随机字符串（验证服务器时返回）
        String timestamp = (valueOf(get("timestamp", ""))); // 时间戳
        String signature = (valueOf(get("signature", ""))); // 签名
        String msg_signature = (valueOf(get("msg_signature", ""))); // 签名（安全模式时返回）
        Logger.getLogger(this.getClass().getName()).info(format("数据：{0}", toJSONString(params)));
        // </editor-fold>

        // <editor-fold defaultstate="collapsed" desc="验证签名">
        WXBizMsgCrypt crypt = null;
        if (isNull(echostr, "").length() > 0) {
            if (this.isWorkWeixin) {
                try {
                    crypt = new WXBizMsgCrypt(this.token, this.encodingAESKey, this.appid);
                    return crypt.verifyUrl(msg_signature, timestamp, nonce, echostr);
                } catch (AesException e) {
                    Logger.getLogger(this.getClass().getName()).log(Level.SEVERE, "签名错误", e);
                    return "签名错误";
                }
            } else {
                try {
                    if (!sha1(true, this.token, timestamp, nonce).equals(signature)) return "签名错误";
                } catch (Exception e) {
                    Logger.getLogger(this.getClass().getName()).log(Level.SEVERE, "签名错误", e);
                    return "签名错误";
                }
                if (isNull(echostr, "").length() > 0) return echostr;
            }
        }
        // </editor-fold>

        // <editor-fold defaultstate="collapsed" desc="判断消息是否解密">
        String encrypt = valueOf(get("Encrypt", ""));
        boolean isEncrypt = !isNull(msg_signature, "").equals("") && !encrypt.equals("");
        if (isEncrypt) {
            try {
                if (!sha1(true, this.token, timestamp, nonce, encrypt).equals(msg_signature)) return "签名错误";
            } catch (UnsupportedEncodingException | NoSuchAlgorithmException e) {
                Logger.getLogger(this.getClass().getName()).log(Level.SEVERE, "签名错误", e);
                return "签名错误";
            }

            try {
                crypt = crypt == null ? new WXBizMsgCrypt(this.token, this.encodingAESKey, this.appid) : crypt;
                String decrypt  = crypt.decrypt(encrypt);
                Map<String, String> decryptMap = xmlToMap(decrypt);
                decryptMap.forEach((t, u) -> params.put(t, u));
            } catch (AesException e) {
                Logger.getLogger(this.getClass().getName()).log(Level.SEVERE, "数据有误", e);
                return "数据有误";
            } catch (Exception e) {
                Logger.getLogger(this.getClass().getName()).log(Level.SEVERE, "解析失败", e);
                return "解析失败";
            }
        }
        // </editor-fold>

        // <editor-fold defaultstate="collapsed" desc="获取消息公共参数">
        String msgType = valueOf(get("MsgType", "")); // 消息类型，event
        String toUserName = valueOf(get("ToUserName", "")); // 开发者微信号
        String createTime = valueOf(get("CreateTime", "")); // 消息创建时间 （整型）
        String fromUserName = valueOf(get("FromUserName", "")); // 发送方帐号（一个OpenID）
        String agentID = valueOf(get("AgentID", "")); // 接收的应用id，可在应用的设置页面获取
        Logger.getLogger(this.getClass().getName()).info(format("数据：{0}", toJSONString(params)));
        this.resultData = new WxSalonResult(nonce, timestamp, isEncrypt, toUserName, openid, fromUserName, msgType, createTime, agentID); // 公共数据
        Object result = resultData; // 返回数据
        // </editor-fold>

        // <editor-fold defaultstate="collapsed" desc="处理消息">
        String qrscene = "qrscene_";
        if (msgType.equalsIgnoreCase("event")) { // 事件消息
            String event = valueOf(get("Event")); // 事件类型，subscribe(订阅)、unsubscribe(取消订阅)、SCAN(用户已关注时的扫描带参数二维码)、LOCATION(上报地理位置事件)、CLICK(自定义菜单事件)
            result = new WxSalonEventResult(event, (WxSalonResult) result);

            switch (event) {
                case "subscribe": { // 订阅
                    String ticket = valueOf(get("Ticket", "")); // 二维码的ticket，可用来换取二维码图片
                    String eventKey = valueOf(get("EventKey", "")); // 事件KEY值，qrscene_为前缀，后面为二维码的参数值
                    if (eventKey != null && eventKey.startsWith(qrscene)) eventKey = eventKey.substring(qrscene.length()); // 去除前缀
                    result = new WxSalonEventTicketResult(ticket, eventKey, (WxSalonEventResult) result);
    
                    Logger.getLogger(this.getClass().getName()).info(format("订阅：{0}", toJSONString(result)));
                    if (options != null) options.Subscribe((WxSalonEventTicketResult) result);
                    break;
                }
                case "unsubscribe": { // 取消订阅
                    Logger.getLogger(this.getClass().getName()).info(format("取消订阅：{0}", toJSONString(result)));
                    if (options != null) options.Unsubscribe((WxSalonEventResult) result);
                    break;
                }
                case "SCAN": { // 用户已关注时的扫描带参数二维码
                    String ticket = valueOf(get("Ticket", "")); // 二维码的ticket，可用来换取二维码图片
                    String eventKey = valueOf(get("EventKey", "")); // 事件KEY值，是一个32位无符号整数，即创建二维码时的二维码scene_id
                    if (eventKey != null && eventKey.startsWith(qrscene)) eventKey = eventKey.substring(qrscene.length()); // 去除前缀
                    result = new WxSalonEventTicketResult(ticket, eventKey, (WxSalonEventResult) result);
    
                    Logger.getLogger(this.getClass().getName()).info(format("用户已关注时的扫描带参数二维码：{0}", toJSONString(result)));
                    if (options != null) options.Scan((WxSalonEventTicketResult) result);
                    break;
                }
                case "LOCATION": { // 上报地理位置事件
                    String latitude = valueOf(get("Latitude", "")); // 地理位置纬度
                    String longitude = valueOf(get("Longitude", "")); // 地理位置经度
                    String precision = valueOf(get("Precision", "")); // 地理位置精度
                    result = new WxSalonEventLocationResult(latitude, longitude, precision, (WxSalonEventResult) result);
    
                    Logger.getLogger(this.getClass().getName()).info(format("上报地理位置事件：{0}", toJSONString(result)));
                    if (options != null) options.Location((WxSalonEventLocationResult) result);
                    break;
                }
                case "CLICK": { // 自定义菜单事件
                    String eventKey = valueOf(get("EventKey", "")); // 事件KEY值，与自定义菜单接口中KEY值对应
                    result = new WxSalonEventKeyResult(eventKey, (WxSalonEventResult) result);
    
                    Logger.getLogger(this.getClass().getName()).info(format("自定义菜单事件：{0}", toJSONString(result)));
                    if (options != null) options.Click((WxSalonEventKeyResult) result);
                    break;
                }
                case "VIEW": { // 点击菜单跳转链接时的事件推送
                    String eventKey = valueOf(get("EventKey", "")); // 事件KEY值，设置的跳转URL
                    result = new WxSalonEventKeyResult(eventKey, (WxSalonEventResult) result);
    
                    Logger.getLogger(this.getClass().getName()).info(format("点击菜单跳转链接时的事件推送：{0}", toJSONString(result)));
                    if (options != null) options.View((WxSalonEventKeyResult) result);
                    break;
                }
                case "TEMPLATESENDJOBFINISH": { // 模板消息发送任务完成时的事件推送
                    String msgID = valueOf(get("MsgID", "")); // 消息ID
                    String status = valueOf(get("Status", "")); // 发送状态为成功:success;用户拒绝接收:failed:user block;发送状态为发送失败（非用户拒绝）:failed: system failed
                    result = new WxSalonEventTemplateSendJobFinishResult(msgID, status, (WxSalonEventResult) result);
    
                    Logger.getLogger(this.getClass().getName()).info(format("模板消息发送任务完成时的事件推送：{0}", toJSONString(result)));
                    if (options != null) options.TemplateSendJobFinish((WxSalonEventTemplateSendJobFinishResult) result);
                    break;
                }
                case "view_miniprogram": { // 点击菜单跳转小程序的事件推送
                    String eventKey = valueOf(get("EventKey", "")); // 事件KEY值，设置的跳转URL
                    String menuID = valueOf(get("MenuID", "")); // 事件KEY值，设置的跳转URL
                    result = new WxSalonEventMenuResult(menuID, eventKey, (WxSalonEventResult) result);
    
                    Logger.getLogger(this.getClass().getName()).info(format("点击菜单跳转小程序的事件推送：{0}", toJSONString(result)));
                    if (options != null) options.ViewMiniProgram((WxSalonEventMenuResult) result);
                    break;
                }
                case "kf_msg_or_event": {
                    String token = valueOf(get("Token", "")); // 调用拉取消息接口时，需要传此token，用于校验请求的合法性
                    String openKfId = valueOf(get("OpenKfId", "")); // 有新消息的客服帐号。可通过sync_msg接口指定open_kfid获取此客服帐号的消息
                    result = new WxSalonEventKfResult(token, openKfId, (WxSalonEventResult) result);

                    Logger.getLogger(this.getClass().getName()).info(format("企业微信客服消息或事件：{0}", toJSONString(result)));
                    if (options != null) options.KfMsgOrEvent((WxSalonEventKfResult) result);
                    break;
                }
                default: {
                    Logger.getLogger(this.getClass().getName()).info(format("未支持事件：{0}", event));
                    if (options != null) options.UnsupportEvent((WxSalonEventResult) result, this.params);
                    break;
                }
            }
        } else if (!msgType.trim().isEmpty()) { // 其他内容消息
            String msgId = valueOf(get("MsgId", "")); // 消息id，64位整型
            String msgDataId = valueOf(get("MsgDataId", "")); // 消息的数据ID（消息如果来自文章时才有）
            String idx = valueOf(get("Idx", "")); // Idx	多图文时第几篇文章，从1开始（消息如果来自文章时才有）
            result = new WxSalonMsgResult(msgId, msgDataId, idx, (WxSalonResult) result);

            switch (msgType) {
                case "text": { // 文本消息
                    String content = valueOf(get("Content", "")); // 文本消息内容
                    String bizMsgMenuId = valueOf(get("bizmsgmenuid", "")); // 点击的菜单ID（消息如果来自菜单时才有）
                    result = new WxSalonMsgTextResult(content, bizMsgMenuId, (WxSalonMsgResult) result);

                    Logger.getLogger(this.getClass().getName()).info(format("文本消息：{0}", toJSONString(result)));
                    if (options != null) options.text((WxSalonMsgTextResult) result);
                    break;
                }
                case "image": { // 图片消息
                    String picUrl = valueOf(get("PicUrl", "")); // 图片链接（由系统生成）
                    String mediaId = valueOf(get("MediaId", "")); // 图片消息媒体id，可以调用多媒体文件下载接口拉取数据。
                    result = new WxSalonMsgImageResult(picUrl, mediaId, (WxSalonMsgResult) result);

                    Logger.getLogger(this.getClass().getName()).info(format("图片消息：{0}", toJSONString(result)));
                    if (options != null) options.image((WxSalonMsgImageResult) result);
                    break;
                }
                case "voice": { // 语音消息
                    String format = valueOf(get("Format", "")); // 语音格式，如amr，speex等
                    String recognition = valueOf(get("Recognition", "")); // 语音识别结果，UTF8编码
                    String mediaId = valueOf(get("MediaId", "")); // 语音消息媒体id，可以调用多媒体文件下载接口拉取数据。
                    result = new WxSalonMsgVoiceResult(mediaId, format, recognition, (WxSalonMsgResult) result);

                    Logger.getLogger(this.getClass().getName()).info(format("语音消息：{0}", toJSONString(result)));
                    if (options != null) options.voice((WxSalonMsgVoiceResult) result);
                    break;
                }
                case "video": { // 视频消息
                    String thumbMediaId = valueOf(get("ThumbMediaId", "")); // 视频消息缩略图的媒体id，可以调用多媒体文件下载接口拉取数据。
                    String mediaId = valueOf(get("MediaId", "")); // 视频消息媒体id，可以调用多媒体文件下载接口拉取数据。
                    result = new WxSalonMsgVideoResult(mediaId, thumbMediaId, (WxSalonMsgResult) result);

                    Logger.getLogger(this.getClass().getName()).info(format("视频消息：{0}", toJSONString(result)));
                    if (options != null) options.video((WxSalonMsgVideoResult) result);
                    break;
                }
                case "shortvideo": { // 小视频消息
                    String thumbMediaId = valueOf(get("ThumbMediaId", "")); // 小视频消息缩略图的媒体id，可以调用多媒体文件下载接口拉取数据。
                    String mediaId = valueOf(get("MediaId", "")); // 小视频消息媒体id，可以调用多媒体文件下载接口拉取数据。
                    result = new WxSalonMsgVideoResult(mediaId, thumbMediaId, (WxSalonMsgResult) result);

                    Logger.getLogger(this.getClass().getName()).info(format("小视频消息：{0}", toJSONString(result)));
                    if (options != null) options.shortvideo((WxSalonMsgVideoResult) result);
                    break;
                }
                case "location": { // 地理位置消息
                    String locationX = valueOf(get("Location_X", "")); // 地理位置维度
                    String locationY = valueOf(get("Location_Y", "")); // 地理位置经度
                    String scale = valueOf(get("Scale", "")); // 地图缩放大小
                    String label = valueOf(get("Label", "")); // 地理位置信息
                    result = new WxSalonMsgLocationResult(locationX, locationY, scale, label, (WxSalonMsgResult) result);

                    Logger.getLogger(this.getClass().getName()).info(format("地理位置消息：{0}", toJSONString(result)));
                    if (options != null) options.location((WxSalonMsgLocationResult) result);
                    break;
                }
                case "link": { // 链接消息
                    String title = valueOf(get("Title", "")); // 消息标题
                    String description = valueOf(get("Description", "")); // 消息描述
                    String url = valueOf(get("Url", "")); // 消息链接
                    result = new WxSalonMsgLinkResult(title, description, url, (WxSalonMsgResult) result);

                    Logger.getLogger(this.getClass().getName()).info(format("链接消息：{0}", toJSONString(result)));
                    if (options != null) options.link((WxSalonMsgLinkResult) result);
                    break;
                }
                default: {
                    Logger.getLogger(this.getClass().getName()).info(format("未支持消息：{0}", msgType));
                    if (options != null) options.UnsupportMsg((WxSalonMsgResult) result, this.params);
                    break;
                }
            }
        }
        // </editor-fold>

        return "";
    }

    /**
     * 获取参数
     * @param key 参数名
     * @return 参数值
     */
    private Object get(String key) {
        return get(key, null);
    }

    /**
     * 获取参数
     * @param key 参数名
     * @param defaultValue 默认值
     * @return 参数值
     */
    private Object get(String key, Object defaultValue) {
        if (params != null && params.containsKey(key)) {
            return params.get(key);
        } else {
            return defaultValue;
        }
    }

    /**
     * XML格式字符串转换为Map
     * @param strXML XML字符串
     * @return XML数据转换后的Map
     * @throws Exception 转换失败时抛出异常
     */
    public static Map<String, String> xmlToMap(String strXML) throws Exception {
        try {
            Map<String, String> data = new HashMap<String, String>();
            DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
            documentBuilderFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
            documentBuilderFactory.setFeature("http://xml.org/sax/features/external-general-entities", false);
            documentBuilderFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
            documentBuilderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
            documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
            documentBuilderFactory.setXIncludeAware(false);
            documentBuilderFactory.setExpandEntityReferences(false);
            DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
            InputStream stream = new ByteArrayInputStream(strXML.getBytes("UTF-8"));
            Document doc = documentBuilder.parse(stream);
            doc.getDocumentElement().normalize();
            NodeList nodeList = doc.getDocumentElement().getChildNodes();
            for (int idx = 0; idx < nodeList.getLength(); ++idx) {
                Node node = nodeList.item(idx);
                if (node.getNodeType() == Node.ELEMENT_NODE) {
                    Element element = (Element) node;
                    data.put(element.getNodeName(), element.getTextContent());
                }
            }
            try {
                stream.close();
            } catch (Exception ex) {
                // do nothing
            }
            return data;
        } catch (Exception e) {
            Logger.getLogger(WxSalon.class.getName()).log(Level.SEVERE, "转换失败", e);
            throw e;
        }
    }

    /**
     * JSON格式字符串转换为Map
     * @param strJSON JSON字符串
     * @return JSON数据转换后的Map
     * @throws Exception 转换失败时抛出异常
     */
    public static Map<String, String> jsonToMap(String strJSON) throws Exception {
        try {
            Map<String, String> data = new HashMap<>();
            JSONObject json = JSONObject.parseObject(strJSON);
            for (String key : json.keySet()) { data.put(key, valueOf(json.get(key))); }
            return data;
        } catch (Exception e) {
            Logger.getLogger(WxSalon.class.getName()).log(Level.SEVERE, "转换失败", e);
            throw e;
        }
    }

    /**
     * 获取被动回复文本消息内容（XML格式）
     * @param content 回复的消息内容（换行：在 content 中能够换行，微信客户端就支持换行显示）
     * @return 被动回复文本消息内容（XML格式）
     */
    public String getText(String content) {
        return getText(content, resultData.getFromUserName(), resultData.getToUserName());
    }

    /**
     * 获取被动回复文本消息内容（XML格式）
     * @param content 回复的消息内容（换行：在 content 中能够换行，微信客户端就支持换行显示）
     * @param toUserName 接收方帐号（收到的OpenID）
     * @param fromUserName 开发者微信号
     * @return 被动回复文本消息内容（XML格式）
     */
    public String getText(String content, String toUserName, String fromUserName) {
        return getText(content, toUserName, fromUserName, false);
    }

    /**
     * 获取被动回复文本消息内容（XML格式）
     * @param content 回复的消息内容（换行：在 content 中能够换行，微信客户端就支持换行显示）
     * @param toUserName 接收方帐号（收到的OpenID）
     * @param fromUserName 开发者微信号
     * @param isEncryp 是否加密
     * @return 被动回复文本消息内容（XML格式）
     */
    public String getText(String content, String toUserName, String fromUserName, boolean isEncryp) {
        return getText(content, toUserName, fromUserName, isEncryp, token, appid, encodingAESKey);
    }

    /**
     * 获取被动回复文本消息内容（XML格式）
     * @param content 回复的消息内容（换行：在 content 中能够换行，微信客户端就支持换行显示）
     * @param toUserName 接收方帐号（收到的OpenID）
     * @param fromUserName 开发者微信号
     * @param isEncryp 是否加密
     * @param token 令牌
     * @param appid 开发者ID
     * @param encodingAESKey 消息加解密密钥
     * @return 被动回复文本消息内容（XML格式）
     */
    public static String getText(String content, String toUserName, String fromUserName, boolean isEncryp, String token, String appid, String encodingAESKey) {
        return getResultData("<xml>"
        + "<ToUserName><![CDATA[" + toUserName + "]]></ToUserName>"
        + "<FromUserName><![CDATA[" + fromUserName + "]]></FromUserName>"
        + "<CreateTime>" + getTimestamp() + "</CreateTime>"
        + "<MsgType><![CDATA[text]]></MsgType>"
        + "<Content><![CDATA[" + content + "]]></Content>"
        + "</xml>", isEncryp, token, appid, encodingAESKey);
    }

    /**
     * 获取被动回复图片消息内容（XML格式）
     * @param mediaId 通过素材管理中的接口上传多媒体文件，得到的id
     * @return 被动回复图片消息内容（XML格式）
     */
    public String getImage(String mediaId) {
        return getImage(mediaId, resultData.getFromUserName(), resultData.getToUserName());
    }

    /**
     * 获取被动回复图片消息内容（XML格式）
     * @param mediaId 通过素材管理中的接口上传多媒体文件，得到的id
     * @param toUserName 接收方帐号（收到的OpenID）
     * @param fromUserName 开发者微信号
     * @return 被动回复图片消息内容（XML格式）
     */
    public String getImage(String mediaId, String toUserName, String fromUserName) {
        return getImage(mediaId, toUserName, fromUserName, false);
    }

    /**
     * 获取被动回复图片消息内容（XML格式）
     * @param mediaId 通过素材管理中的接口上传多媒体文件，得到的id
     * @param toUserName 接收方帐号（收到的OpenID）
     * @param fromUserName 开发者微信号
     * @param isEncryp 是否加密
     * @return 被动回复图片消息内容（XML格式）
     */
    public String getImage(String mediaId, String toUserName, String fromUserName, boolean isEncryp) {
        return getImage(mediaId, toUserName, fromUserName, isEncryp, token, appid, encodingAESKey);
    }

    /**
     * 获取被动回复图片消息内容（XML格式）
     * @param mediaId 通过素材管理中的接口上传多媒体文件，得到的id
     * @param toUserName 接收方帐号（收到的OpenID）
     * @param fromUserName 开发者微信号
     * @param isEncryp 是否加密
     * @param token 令牌
     * @param appid 开发者ID
     * @param encodingAESKey 消息加解密密钥
     * @return 被动回复图片消息内容（XML格式）
     */
    public static String getImage(String mediaId, String toUserName, String fromUserName, boolean isEncryp, String token, String appid, String encodingAESKey) {
        return getResultData("<xml>"
        + "<ToUserName><![CDATA[" + toUserName + "]]></ToUserName>"
        + "<FromUserName><![CDATA[" + fromUserName + "]]></FromUserName>"
        + "<CreateTime>" + getTimestamp() + "</CreateTime>"
        + "<MsgType><![CDATA[image]]></MsgType>"
        + "<Image>"
        + "<MediaId><![CDATA[" + mediaId + "]]></MediaId>"
        + "</Image>"
        + "</xml>", isEncryp, token, appid, encodingAESKey);
    }

    /**
     * 获取被动回复语音消息内容（XML格式）
     * @param mediaId 通过素材管理中的接口上传多媒体文件，得到的id
     * @return 被动回复语音消息内容（XML格式）
     */
    public String getVoice(String mediaId) {
        return getVoice(mediaId, resultData.getFromUserName(), resultData.getToUserName());
    }

    /**
     * 获取被动回复语音消息内容（XML格式）
     * @param mediaId 通过素材管理中的接口上传多媒体文件，得到的id
     * @param toUserName 接收方帐号（收到的OpenID）
     * @param fromUserName 开发者微信号
     * @return 被动回复语音消息内容（XML格式）
     */
    public String getVoice(String mediaId, String toUserName, String fromUserName) {
        return getVoice(mediaId, toUserName, fromUserName, false);
    }

    /**
     * 获取被动回复语音消息内容（XML格式）
     * @param mediaId 通过素材管理中的接口上传多媒体文件，得到的id
     * @param toUserName 接收方帐号（收到的OpenID）
     * @param fromUserName 开发者微信号
     * @param isEncryp 是否加密
     * @return 被动回复语音消息内容（XML格式）
     */
    public String getVoice(String mediaId, String toUserName, String fromUserName, boolean isEncryp) {
        return getVoice(mediaId, toUserName, fromUserName, isEncryp, token, appid, encodingAESKey);
    }

    /**
     * 获取被动回复语音消息内容（XML格式）
     * @param mediaId 通过素材管理中的接口上传多媒体文件，得到的id
     * @param toUserName 接收方帐号（收到的OpenID）
     * @param fromUserName 开发者微信号
     * @param isEncryp 是否加密
     * @param token 令牌
     * @param appid 开发者ID
     * @param encodingAESKey 消息加解密密钥
     * @return 被动回复语音消息内容（XML格式）
     */
    public static String getVoice(String mediaId, String toUserName, String fromUserName, boolean isEncryp, String token, String appid, String encodingAESKey) {
        return getResultData("<xml>"
        + "<ToUserName><![CDATA[" + toUserName + "]]></ToUserName>"
        + "<FromUserName><![CDATA[" + fromUserName + "]]></FromUserName>"
        + "<CreateTime>" + getTimestamp() + "</CreateTime>"
        + "<MsgType><![CDATA[voice]]></MsgType>"
        + "<Voice>"
        + "<MediaId><![CDATA[" + mediaId + "]]></MediaId>"
        + "</Voice>"
        + "</xml>", isEncryp, token, appid, encodingAESKey);
    }

    /**
     * 获取被动回复视频消息内容（XML格式）
     * @param mediaId 通过素材管理中的接口上传多媒体文件，得到的id
     * @param title 视频消息的标题
     * @param description 视频消息的描述
     * @return 被动回复视频消息内容（XML格式）
     */
    public String getVideo(String mediaId, String title, String description) {
        return getVideo(mediaId, title, description, resultData.getFromUserName(), resultData.getToUserName());
    }

    /**
     * 获取被动回复视频消息内容（XML格式）
     * @param mediaId 通过素材管理中的接口上传多媒体文件，得到的id
     * @param title 视频消息的标题
     * @param description 视频消息的描述
     * @param toUserName 接收方帐号（收到的OpenID）
     * @param fromUserName 开发者微信号
     * @return 被动回复视频消息内容（XML格式）
     */
    public String getVideo(String mediaId, String title, String description, String toUserName, String fromUserName) {
        return getVideo(mediaId, title, description, toUserName, fromUserName, false);
    }

    /**
     * 获取被动回复视频消息内容（XML格式）
     * @param mediaId 通过素材管理中的接口上传多媒体文件，得到的id
     * @param title 视频消息的标题
     * @param description 视频消息的描述
     * @param toUserName 接收方帐号（收到的OpenID）
     * @param fromUserName 开发者微信号
     * @param isEncryp 是否加密
     * @return 被动回复视频消息内容（XML格式）
     */
    public String getVideo(String mediaId, String title, String description, String toUserName, String fromUserName, boolean isEncryp) {
        return getVideo(mediaId, title, description, toUserName, fromUserName, isEncryp, token, appid, encodingAESKey);
    }

    /**
     * 获取被动回复视频消息内容（XML格式）
     * @param mediaId 通过素材管理中的接口上传多媒体文件，得到的id
     * @param title 视频消息的标题
     * @param description 视频消息的描述
     * @param toUserName 接收方帐号（收到的OpenID）
     * @param fromUserName 开发者微信号
     * @param isEncryp 是否加密
     * @param token 令牌
     * @param appid 开发者ID
     * @param encodingAESKey 消息加解密密钥
     * @return 被动回复视频消息内容（XML格式）
     */
    public static String getVideo(String mediaId, String title, String description, String toUserName, String fromUserName, boolean isEncryp, String token, String appid, String encodingAESKey) {
        return getResultData("<xml>"
        + "<ToUserName><![CDATA[" + toUserName + "]]></ToUserName>"
        + "<FromUserName><![CDATA[" + fromUserName + "]]></FromUserName>"
        + "<CreateTime>" + getTimestamp() + "</CreateTime>"
        + "<MsgType><![CDATA[video]]></MsgType>"
        + "<Video>"
        + "<MediaId><![CDATA[" + mediaId + "]]></MediaId>"
        + "<Title><![CDATA[" + title + "]]></Title>"
        + "<Description><![CDATA[" + description + "]]></Description>"
        + "</Video>"
        + "</xml>", isEncryp, token, appid, encodingAESKey);
    }

    /**
     * 获取被动回复音乐消息内容（XML格式）
     * @param title 音乐标题
     * @param description 音乐描述
     * @param musicURL 音乐链接
     * @param hQMusicUrl 高质量音乐链接，WIFI环境优先使用该链接播放音乐
     * @param thumbMediaId 缩略图的媒体id，通过素材管理中的接口上传多媒体文件，得到的id
     * @return 被动回复音乐消息内容（XML格式）
     */
    public String getMusic(String title, String description, String musicURL, String hQMusicUrl, String thumbMediaId) {
        return getMusic(title, description, musicURL, hQMusicUrl, thumbMediaId, resultData.getFromUserName(), resultData.getToUserName());
    }

    /**
     * 获取被动回复音乐消息内容（XML格式）
     * @param title 音乐标题
     * @param description 音乐描述
     * @param musicURL 音乐链接
     * @param hQMusicUrl 高质量音乐链接，WIFI环境优先使用该链接播放音乐
     * @param thumbMediaId 缩略图的媒体id，通过素材管理中的接口上传多媒体文件，得到的id
     * @param toUserName 接收方帐号（收到的OpenID）
     * @param fromUserName 开发者微信号
     * @return 被动回复音乐消息内容（XML格式）
     */
    public String getMusic(String title, String description, String musicURL, String hQMusicUrl, String thumbMediaId, String toUserName, String fromUserName) {
        return getMusic(title, description, musicURL, hQMusicUrl, thumbMediaId, toUserName, fromUserName, false);
    }

    /**
     * 获取被动回复音乐消息内容（XML格式）
     * @param title 音乐标题
     * @param description 音乐描述
     * @param musicURL 音乐链接
     * @param hQMusicUrl 高质量音乐链接，WIFI环境优先使用该链接播放音乐
     * @param thumbMediaId 缩略图的媒体id，通过素材管理中的接口上传多媒体文件，得到的id
     * @param toUserName 接收方帐号（收到的OpenID）
     * @param fromUserName 开发者微信号
     * @param isEncryp 是否加密
     * @return 被动回复音乐消息内容（XML格式）
     */
    public String getMusic(String title, String description, String musicURL, String hQMusicUrl, String thumbMediaId, String toUserName, String fromUserName, boolean isEncryp) {
        return getMusic(title, description, musicURL, hQMusicUrl, thumbMediaId, toUserName, fromUserName, isEncryp, token, appid, encodingAESKey);
    }

    /**
     * 获取被动回复音乐消息内容（XML格式）
     * @param title 音乐标题
     * @param description 音乐描述
     * @param musicURL 音乐链接
     * @param hQMusicUrl 高质量音乐链接，WIFI环境优先使用该链接播放音乐
     * @param thumbMediaId 缩略图的媒体id，通过素材管理中的接口上传多媒体文件，得到的id
     * @param toUserName 接收方帐号（收到的OpenID）
     * @param fromUserName 开发者微信号
     * @param isEncryp 是否加密
     * @param token 令牌
     * @param appid 开发者ID
     * @param encodingAESKey 消息加解密密钥
     * @return 被动回复音乐消息内容（XML格式）
     */
    public static String getMusic(String title, String description, String musicURL, String hQMusicUrl, String thumbMediaId, String toUserName, String fromUserName, boolean isEncryp, String token, String appid, String encodingAESKey) {
        return getResultData("<xml>"
        + "<ToUserName><![CDATA[" + toUserName + "]]></ToUserName>"
        + "<FromUserName><![CDATA[" + fromUserName + "]]></FromUserName>"
        + "<CreateTime>" + getTimestamp() + "</CreateTime>"
        + "<MsgType><![CDATA[music]]></MsgType>"
        + "<Music>"
        + "<Title><![CDATA[" + title + "]]></Title>"
        + "<Description><![CDATA[" + description + "]]></Description>"
        + "<MusicUrl><![CDATA[" + musicURL + "]]></MusicUrl>"
        + "<HQMusicUrl><![CDATA[" + hQMusicUrl + "]]></HQMusicUrl>"
        + "<ThumbMediaId><![CDATA[" + thumbMediaId + "]]></ThumbMediaId>"
        + "</Music>"
        + "</xml>", isEncryp, token, appid, encodingAESKey);
    }

    /**
     * 获取被动回复文章消息内容（XML格式）
     * @param title 图文消息标题
     * @param description 图文消息描述
     * @param picurl 图片链接，支持JPG、PNG格式，较好的效果为大图360*200，小图200*200
     * @param url 点击图文消息跳转链接
     * @return 被动回复文章消息内容（XML格式）
     */
    public String getNews(String title, String description, String picurl, String url) {
        return getNews(title, description, picurl, url, resultData.getFromUserName(), resultData.getToUserName());
    }

    /**
     * 获取被动回复文章消息内容（XML格式）
     * @param title 图文消息标题
     * @param description 图文消息描述
     * @param picurl 图片链接，支持JPG、PNG格式，较好的效果为大图360*200，小图200*200
     * @param url 点击图文消息跳转链接
     * @param toUserName 接收方帐号（收到的OpenID）
     * @param fromUserName 开发者微信号
     * @return 被动回复文章消息内容（XML格式）
     */
    public String getNews(String title, String description, String picurl, String url, String toUserName, String fromUserName) {
        return getNews(title, description, picurl, url, toUserName, fromUserName, false);
    }

    /**
     * 获取被动回复文章消息内容（XML格式）
     * @param title 图文消息标题
     * @param description 图文消息描述
     * @param picurl 图片链接，支持JPG、PNG格式，较好的效果为大图360*200，小图200*200
     * @param url 点击图文消息跳转链接
     * @param toUserName 接收方帐号（收到的OpenID）
     * @param fromUserName 开发者微信号
     * @param isEncryp 是否加密
     * @return 被动回复文章消息内容（XML格式）
     */
    public String getNews(String title, String description, String picurl, String url, String toUserName, String fromUserName, boolean isEncryp) {
        return getNews(title, description, picurl, url, toUserName, fromUserName, isEncryp, token, appid, encodingAESKey);
    }

    /**
     * 获取被动回复文章消息内容（XML格式）
     * @param title 图文消息标题
     * @param description 图文消息描述
     * @param picurl 图片链接，支持JPG、PNG格式，较好的效果为大图360*200，小图200*200
     * @param url 点击图文消息跳转链接
     * @param toUserName 接收方帐号（收到的OpenID）
     * @param fromUserName 开发者微信号
     * @param isEncryp 是否加密
     * @param token 令牌
     * @param appid 开发者ID
     * @param encodingAESKey 消息加解密密钥
     * @return 被动回复文章消息内容（XML格式）
     */
    public static String getNews(String title, String description, String picurl, String url, String toUserName, String fromUserName, boolean isEncryp, String token, String appid, String encodingAESKey) {
        return getResultData("<xml>"
        + "<ToUserName><![CDATA[" + toUserName + "]]></ToUserName>"
        + "<FromUserName><![CDATA[" + fromUserName + "]]></FromUserName>"
        + "<CreateTime>" + getTimestamp() + "</CreateTime>"
        + "<MsgType><![CDATA[news]]></MsgType>"
        + "<ArticleCount>1</ArticleCount>"
        + "<Articles>"
        + "<item>"
        + "<Title><![CDATA[" + title + "]]></Title>"
        + "<Description><![CDATA[" + description + "]]></Description>"
        + "<PicUrl><![CDATA[" + picurl + "]]></PicUrl>"
        + "<Url><![CDATA[" + url + "]]></Url>"
        + "</item>"
        + "</Articles>"
        + "</xml>", isEncryp, token, appid, encodingAESKey);
    }

    /**
     * 获取被动回复消息内容（XML格式）
     * @param data 消息数据
     * @return 被动回复消息内容（XML格式）
     */
    public String getResultData(String data) {
        return getResultData(data, false, token, appid, encodingAESKey);
    }

    /**
     * 获取被动回复消息内容（XML格式）
     * @param data 消息数据
     * @param isEncryp 是否加密
     * @param token 令牌
     * @param appid 开发者ID
     * @param encodingAESKey 消息加解密密钥
     * @return 被动回复消息内容（XML格式）
     */
    public static String getResultData(String data, boolean isEncryp, String token, String appid, String encodingAESKey) {
        if (isEncryp) {
            try {
                String nonce = getNonceStr(32);
                String timestamp = valueOf(getTimestamp());
                WXBizMsgCrypt crypt = new WXBizMsgCrypt(token, encodingAESKey, appid);
                String encrypt = crypt.encryptMsg(data, timestamp, nonce);

                return "<xml>\n"
                        + "<Encrypt><![CDATA[" + encrypt + "]]></Encrypt>\n"
                        + "<MsgSignature><![CDATA[" + SHA1.getSHA1(token, timestamp, nonce, encrypt) + "]]></MsgSignature>\n"
                        + "<TimeStamp>" + timestamp + "</TimeStamp>\n"
                        + "<Nonce><![CDATA[" + nonce + "]]></Nonce>\n"
                        + "</xml>";
            } catch (AesException e) {
                Logger.getLogger(WxSalon.class.getName()).log(Level.SEVERE, "加密失败", e);
                return data;
            }
        } else {
            return data;
        }
    }

    /**
     * 获取小程序链接
     * @param title 标题
     * @param appid 小程序的appid
     * @param path 小程序的页面路径
     * @param url 点击后跳转的链接
     * @return 小程序链接
     */
    public String getMpLink(String title, String appid, String path, String url) {
        return "<a data-miniprogram-type=\"text\" data-miniprogram-appid=\"" + appid + "\" data-miniprogram-path=\"" + path + "\" href=\"" + url + "\">" + title + "</a>";
    }
}
